import dayjs from 'dayjs';
import partition from 'lodash/partition';
import { encryptSessionKey } from 'skiff-crypto';
import { AttendeeStatus, AttendeePermission, PublicKey, SendAddressRequest } from 'skiff-graphql';
import { ParsedAttendee, AttendeeRole, AttendeeStatus as ParsedAttendeeStatus, ParsedOrganizer } from 'skiff-ics';

import { PulledCalendarEventsFragment } from '../../../generated/graphql';
import { attendeeMatchAnyAlias } from '../../utils/attendeeUtils';
import { resolveAttendees } from '../../utils/userUtils';

import { CalendarMetadataDB } from './CalendarMetadata';
import { DecryptedEventModel } from './event/DecryptedEventModel';
import {
  EventAttendee,
  EventAttendeeType,
  UnresolvedAttendee,
  InternalAttendeeWithEncryptedSessionKey,
  ExternalAttendee,
  isInternalAttendeeWithPublicKey,
  InternalAttendee,
  isInternalAttendeeWithEncryptedSessionKey,
  isUnresolvedAttendee
} from './event/types';

export const organizerValueToMail = (organizer: string) => {
  // Organizer value starts with mailto:
  return organizer.replace('mailto:', '');
};

export function attendeeFromParsedICS(
  parsedAttendee: ParsedAttendee,
  organizer?: ParsedOrganizer,
  fromScheduler?: boolean
): EventAttendee {
  const optional = parsedAttendee.role === AttendeeRole.Optional;
  const isOrganizer = organizer && parsedAttendee.email === organizerValueToMail(organizer.value);

  const attendeeStatus = ((status: ParsedAttendeeStatus) => {
    // If the ICS is from a scheduler like cal.com, mark all attendee statuses as "Yes"
    if (fromScheduler) {
      return AttendeeStatus.Yes;
    }
    switch (status) {
      case ParsedAttendeeStatus.Accepted:
        return AttendeeStatus.Yes;
      case ParsedAttendeeStatus.Declined:
        return AttendeeStatus.No;
      case ParsedAttendeeStatus.Tentative:
        return AttendeeStatus.Maybe;
      case ParsedAttendeeStatus.NeedActions:
      case ParsedAttendeeStatus.Delegated:
        return AttendeeStatus.Pending;
    }
  })(parsedAttendee.status);

  return {
    id: parsedAttendee.email,
    attendeeStatus, // transform status
    type: fromScheduler ? EventAttendeeType.UnresolvedAttendee : EventAttendeeType.ExternalAttendee, // if the ICS is from a scheduler, we manually resolve attendees later
    permission: isOrganizer ? AttendeePermission.Owner : AttendeePermission.Read,
    displayName: parsedAttendee.name,
    optional,
    email: parsedAttendee.email,
    updatedAt: dayjs().valueOf(),
    deleted: false
  };
}

export async function attendeesFromParsedAttendees(
  parsedAttendees: ParsedAttendee[],
  organizer?: ParsedOrganizer,
  fromScheduler?: boolean
) {
  const eventAttendees: EventAttendee[] = parsedAttendees.map((attendee) =>
    attendeeFromParsedICS(attendee, organizer, fromScheduler)
  );
  const unresolvedAttendees = eventAttendees.filter((attendee) =>
    isUnresolvedAttendee(attendee)
  ) as UnresolvedAttendee[];
  // If it's from a scheduler like cal.com, the guests could be internal
  // Skiff users since the event was not created within Skiff
  const attendees = fromScheduler ? await resolveAttendees(unresolvedAttendees) : eventAttendees;
  if (!organizer) return attendees;
  // Check if organizer is in attendees
  const organizerMail = organizerValueToMail(organizer.value);
  const organizerInAttendees = attendees.find((attendee) => attendee.email === organizerMail);
  // If organizer does exist and is not in attendees, add organizer (this happens in ics files generated by outlook)
  if (organizer && !organizerInAttendees) {
    const organizerAttendee: EventAttendee = {
      id: organizerMail,
      attendeeStatus: AttendeeStatus.Yes, // transform status
      type: fromScheduler ? EventAttendeeType.UnresolvedAttendee : EventAttendeeType.ExternalAttendee, // if the ICS is from a scheduler, we manually resolve the organizer later
      permission: AttendeePermission.Owner,
      displayName: organizer.name,
      optional: false,
      email: organizerMail,
      updatedAt: dayjs().valueOf(),
      deleted: false
    };
    const resolvedOrganizerAttendee = isUnresolvedAttendee(organizerAttendee)
      ? await resolveAttendees([organizerAttendee])
      : [organizerAttendee];
    attendees.push(resolvedOrganizerAttendee[0]);
  }

  return attendees;
}

export function updateAttendee(
  parsedAttendee: ParsedAttendee,
  existingEvent: DecryptedEventModel,
  allUserAliases: string[],
  isOrganizer: boolean
): EventAttendee | undefined {
  const translatedAttendee = attendeeFromParsedICS(parsedAttendee);
  const existingAttendees = existingEvent.decryptedContent.attendees;
  const existingAttendee = existingAttendees.find(({ email }) => parsedAttendee.email === email);

  // if the attendee does not exist
  if (!existingAttendee) {
    const currentUserAttendee = existingAttendees.find((attendee) => attendeeMatchAnyAlias(attendee, allUserAliases));
    const translatedAttendeeIsOwnerAlias = attendeeMatchAnyAlias(translatedAttendee, allUserAliases);

    // we should enable only one alias of the user to be in the attendees list to prevent duplicate evens for the same user in the DB
    if (currentUserAttendee && translatedAttendeeIsOwnerAlias) {
      // make sure that if one of the users aliases is the owner, the alias we keep will also be the owner
      if (isOrganizer && currentUserAttendee.permission !== AttendeePermission.Owner) {
        existingEvent.updateAttendee(currentUserAttendee.id, { permission: AttendeePermission.Owner });
      }
      return undefined;
    }

    return translatedAttendee;
  }

  // if the attendee already exists, only partial details
  return {
    ...existingAttendee,
    attendeeStatus: translatedAttendee.attendeeStatus,
    displayName: translatedAttendee.displayName,
    optional: translatedAttendee.optional,
    email: translatedAttendee.email,
    updatedAt: translatedAttendee.updatedAt
  };
}

export function attendeesFromGraphql(
  internalAttendeeList: PulledCalendarEventsFragment['internalAttendeeList']
): InternalAttendee[] {
  return internalAttendeeList.map((attendee) => ({
    id: attendee.calendarID,
    type: EventAttendeeType.InternalAttendee,
    attendeeStatus: attendee.status,
    ...attendee,
    displayName: attendee.displayName ?? ''
  }));
}

/**
 * Lazily generate and update attendees that haven't had encrypted session keys generated yet.
 *
 * This function first resolves any unresolved attendees, then generates session keys for any InternalAttendeesWithPublicKey.
 *
 * After running, only InternalAttendeesWithSessionKey and ExternalAttendees will remain.
 */
export async function encryptSessionKeysForAttendees(
  calendar: CalendarMetadataDB,
  userPrivateKey: string,
  userPublicKey: PublicKey,
  sessionKey: string,
  attendees: EventAttendee[]
) {
  const decryptedCalendarPrivateKey = calendar.getDecryptedCalendarPrivateKey(userPrivateKey, userPublicKey);
  const [unresolvedAttendees, otherAttendees] = partition<EventAttendee, UnresolvedAttendee>(
    attendees,
    (attendee): attendee is UnresolvedAttendee => {
      return attendee.type === EventAttendeeType.UnresolvedAttendee;
    }
  );

  const resolvedAttendees = otherAttendees.concat(
    unresolvedAttendees.length ? await resolveAttendees(unresolvedAttendees) : []
  );
  const newAttendeesList = await Promise.all(
    resolvedAttendees.map((attendee): InternalAttendeeWithEncryptedSessionKey | ExternalAttendee => {
      if (isInternalAttendeeWithPublicKey(attendee) && !isInternalAttendeeWithEncryptedSessionKey(attendee)) {
        const encryptedSessionKey = encryptSessionKey(
          sessionKey,
          decryptedCalendarPrivateKey,
          { key: calendar.publicKey },
          attendee.publicKey
        );
        return {
          ...attendee,
          encryptedSessionKey: encryptedSessionKey.encryptedKey,
          encryptedByKey: encryptedSessionKey.encryptedBy.key
        };
      }
      return attendee;
    })
  );
  return newAttendeesList;
}

export function mergeAttendees(
  currentAttendees: EventAttendee[] | undefined,
  newAttendees: EventAttendee[]
): {
  mergedAttendees: EventAttendee[];
  updatedAt: number;
  contentConflict: boolean;
  rsvpConflict: boolean;
} {
  const now = dayjs().valueOf();

  const currentAttendeesIDs = (currentAttendees || []).map((attendee) => attendee.id);
  const newAttendeesIDs = (newAttendees || []).map((attendee) => attendee.id);

  const allAttendeesIDs = new Set([...currentAttendeesIDs, ...newAttendeesIDs]);

  const currentAttendeesMap = new Map<string, EventAttendee>(
    (currentAttendees || []).map((attendee) => [attendee.id, attendee])
  );
  const newAttendeesMap = new Map<string, EventAttendee>(
    (newAttendees || []).map((attendee) => [attendee.id, attendee])
  );

  const mergedAttendees: EventAttendee[] = [];

  let contentConflict = false;
  let rsvpConflict = false;

  allAttendeesIDs.forEach((attendeeUniqueID) => {
    const currentAttendee = currentAttendeesMap.get(attendeeUniqueID);
    const newAttendee = newAttendeesMap.get(attendeeUniqueID);

    if (!currentAttendee || !newAttendee) {
      // a new attendee was added remotely
      if (!currentAttendee && newAttendee) {
        mergedAttendees.push(newAttendee);
        return;
      }
      // a new attendee was added locally
      else if (currentAttendee && !newAttendee) {
        mergedAttendees.push(currentAttendee);
        contentConflict = true;
        return;
      }

      return;
    }

    const currentUpdatedAt = currentAttendee.updatedAt || 0;
    const newUpdatedAt = newAttendee.updatedAt || 0;

    // if the local attendee was updated after the remote
    // i.e the local attendee has a newer version
    if (currentUpdatedAt > newUpdatedAt) {
      mergedAttendees.push(currentAttendee);
      // if the attendee has updated that means he changed his rsvp status
      rsvpConflict = true;
      return;
    }
    mergedAttendees.push({ ...currentAttendee, ...newAttendee });
  });

  return {
    mergedAttendees: mergedAttendees,
    updatedAt: now,
    rsvpConflict,
    contentConflict
  };
}

/**
 * Produces list of addresses for a list attendees which will be used for sending emails
 * Intentionally static to avoid any filtering based on the role/permission of attendees in the list.
 *
 * @param attendees - List of attendees
 * @returns List of addresses
 */
export function attendeeListToAddresses(attendees: EventAttendee[]): SendAddressRequest[] {
  return attendees.map((attendee) => ({
    name: attendee.displayName ?? attendee.email,
    address: attendee.email
  }));
}
